package de.reondo.jos;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.jgrapht.Graph;
import org.jgrapht.alg.DijkstraShortestPath;
import org.jgrapht.graph.DirectedMultigraph;

import de.reondo.jos.filters.IFilter;

public class ClassAnalyser {

    private IFilter<Class<?>> filter;
    private Graph<ClassInfo, ClassInfoDependencyEdge> graph;

    public ClassAnalyser() {
        this.graph = new DirectedMultigraph<ClassInfo, ClassInfoDependencyEdge>(
               ClassInfoDependencyEdge.class);

        //this.graph = new DefaultDirectedGraph<Class<?>, ClassDependencyEdge>(ClassDependencyEdge.class);
    }

    public void extend(Class<?> rootClass) {
        extendGraph(rootClass);
    }
    
    // Add a dependency between src and classes occurring in Type tgt, for all
    // tgt's classes that are not filtered
    private void addEdges(ClassInfo src, Type tgtType, ClassDependencyType type, String occurrence, List<Class<?>> worklist) {
        Set<Class<?>> occurringClasses = getClassesOccurringInClass(tgtType);
        graph.addVertex(src);
        for (Class<?> c : occurringClasses) {
            if (accept(c)) {
                ClassInfo tgt = new ClassInfo(c);
                graph.addVertex(tgt);
                worklist.add(c);
                graph.addEdge(src, tgt, new ClassInfoDependencyEdge(type, src, tgt, occurrence));
            }
        }
    }

    private Set<Class<?>> getClassesOccurringInClass(Type type) {
        HashSet<Class<?>> set = new HashSet<Class<?>>();
        addClassesOfGenericType(type, set);
        return set;
    }

    private void addClassOfBasicType(Class<?> cls, Set<Class<?>> set) {
        if (cls.isArray()) {
            addClassOfBasicType(cls.getComponentType(), set);
        } else {
            set.add(cls);
        }
    }

    private void addClassesOfGenericType(Type t, Set<Class<?>> set) {
        if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            Type[] typeArgs = pt.getActualTypeArguments();
            for (Type typeArg : typeArgs) {
                addClassesOfGenericType(typeArg, set);
            }
            addClassOfBasicType((Class<?>) pt.getRawType(), set);
        } else if (t instanceof TypeVariable<?>) {
        } else if (t instanceof GenericArrayType) {
            addClassesOfGenericType(
                    ((GenericArrayType) t).getGenericComponentType(), set);
        } else if (t instanceof WildcardType) {
        } else if (t instanceof Class<?>) {
            addClassOfBasicType((Class<?>) t, set);
        } else {
            throw new IllegalArgumentException("Unknown Type: " + t);
        }
    }

    private boolean accept(Class<?> cls) {
        return filter == null || filter.accept(cls);
    }

    private void analyseClass(Class<?> cls, ArrayList<Class<?>> worklist) {
        if (cls.isPrimitive())
            return;
        ClassInfo clsInfo = new ClassInfo(cls);
        // constructors
        for (Constructor<?> c : cls.getDeclaredConstructors()) {
            String occurrence = c.toGenericString();
            for (Type arg : c.getGenericParameterTypes()) {
                addEdges(clsInfo, arg,
                        ClassDependencyType.CONSTRUCTOR_PARAMETER, occurrence,
                        worklist);
            }
            for (Type exn : c.getGenericExceptionTypes()) {
                addEdges(clsInfo, exn,
                        ClassDependencyType.CONSTRUCTOR_EXCEPTION, occurrence,
                        worklist);
            }
        }
        // methods
        for (Method m : cls.getDeclaredMethods()) {
            Type rt = m.getGenericReturnType();
            String occurrence = m.toGenericString();
            addEdges(clsInfo, rt, ClassDependencyType.METHOD_RETURN_TYPE,
                    occurrence, worklist);
            for (Type arg : m.getGenericParameterTypes()) {
                addEdges(clsInfo, arg, ClassDependencyType.METHOD_PARAMETER,
                        occurrence, worklist);
            }
        }
        // fields
        for (Field f : cls.getDeclaredFields()) {
            Type ft = f.getGenericType();
            addEdges(clsInfo, ft, ClassDependencyType.FIELD,
                    f.toGenericString(), worklist);
        }
        // superclass
        Type sclass = cls.getGenericSuperclass();
        if (sclass != null) {
            addEdges(clsInfo, sclass, ClassDependencyType.SUPERCLASS,
                    cls.getName() + " extends " + sclass.toString(), worklist);
        }
        // interfaces
        for (Type iface : cls.getGenericInterfaces()) {
            addEdges(clsInfo, iface, ClassDependencyType.INTERFACE,
                    cls.getName() + " implements " + iface.toString(), worklist);
        }
    }

    private void extendGraph(Class<?> root) {
        ArrayList<Class<?>> worklist = new ArrayList<Class<?>>();
        worklist.add(root);
        while (!worklist.isEmpty()) {
            Class<?> currentClass = worklist.remove(worklist.size() - 1);
            if (graph.containsVertex(new ClassInfo(currentClass))) {
                continue;
            }
            analyseClass(currentClass, worklist);
        }
    }

    /**
     * Returns an array containing the occurrences of the edges that lead from
     * the source class to the target class. The array is ordered starting from
     * the source.
     */
    public String[] getTrace(ClassInfo source, ClassInfo target) {
        List<ClassInfoDependencyEdge> path = DijkstraShortestPath.findPathBetween(
                graph, source, target);
        if (path == null) {
            return null;
        }
        String[] result = new String[path.size()];
        int i = 0;
        for (ClassInfoDependencyEdge edge : path) {
            result[i++] = edge.getOccurrence();
        }
        return result;
    }

    public Graph<ClassInfo, ClassInfoDependencyEdge> getGraph() {
        return graph;
    }

    public IFilter<Class<?>> getFilter() {
        return filter;
    }

    public void setFilter(IFilter<Class<?>> filter) {
        this.filter = filter;
    }

}
